#ifndef MATERIAL_H
#define MATERIAL_H

#include "common/rtweekend.h"

vec3 random_in_unit_sphere() {
  vec3 p;
  do {
    // pick a random point in unit square {x,y,z} in range -1 to +1
    p = 2.0*vec3(random_double(), random_double(), random_double()) - vec3(1,1,1);
  } while (p.squared_length() >= 1.0); // must be within unit sphere
  return p;
}

// angle of incidence is the angle of reflection
// n is unit normal vector
vec3 reflect(const vec3& v, const vec3& n) {
  return v - 2*dot(v,n)*n;
}

// Snell's law: n_0 sin(theta_0) = n_1 sin(theta_1)
// n_0: originating material index of refraction
// n_1: transmitting material index of refraction
// theta_0, theta_1, angle of ray w.r.t. normal
// (n_0 / n_1) * sin(theta_0) = sin (theta_1)
// if n_0 > n_1, there may be no real solution (total internal reflection)
// theta_1 = arcsin( (n_0/ n_1) * sin(theta_0)
// "critical angle" is when theta_1 = 90°

// a = cos(theta_0)*v
// x = sin(theta_0)*v
// sin(theta_0) = x / sqrt(a^2 + x^2)
// cos(theta_0) = dot(uv, n)

// n is unit normal vector
bool refract(const vec3& v, const vec3& n, float ni_over_nt, vec3& refracted) {
  vec3 uv = unit_vector(v);
  float dt = dot(uv, n);
  // TBD: prove these derivations
  float discriminant = 1.0 - ni_over_nt*ni_over_nt*(1-dt*dt);
  if (discriminant > 0) {
    refracted = ni_over_nt*(uv - n*dt) - n*sqrt(discriminant);
    return true;
  }
  else
    return false;
}

float schlick(float cosine, float ref_idx) {
  float r0 = (1-ref_idx) / (1+ref_idx);
  r0 = r0*r0;
  return r0 + (1-r0)*pow((1 - cosine),5);
}


class material  {
public:
  virtual bool scatter(
                       const ray& r_in, const hit_record& rec, vec3& attenuation,
                       ray& scattered) const = 0;
};


class lambertian : public material {
public:
  lambertian(const vec3& a) : albedo(a) {}
  virtual bool scatter(const ray& r_in, const hit_record& rec,
                       vec3& attenuation, ray& scattered) const {
    // consume to avoid compiler warning
    r_in.direction();
    vec3 target = rec.p + rec.normal + random_in_unit_sphere();
    scattered = ray(rec.p, target-rec.p);
    attenuation = albedo;
    return true;
  }

  vec3 albedo;
};


class metal : public material {
public:
  metal(const vec3& a, float f) : albedo(a) {
    if (f < 1) fuzz = f; else fuzz = 1;
  }
  virtual bool scatter(const ray& r_in, const hit_record& rec,
                       vec3& attenuation, ray& scattered) const {
    vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
    scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere());
    attenuation = albedo;
    return (dot(scattered.direction(), rec.normal) > 0);
  }
  vec3 albedo;
  float fuzz;
};


class dielectric : public material {
public:
  dielectric(float ri) : ref_idx(ri) {}
  virtual bool scatter(const ray& r_in, const hit_record& rec,
                       vec3& attenuation, ray& scattered) const {
    vec3 outward_normal;
    vec3 reflected = reflect(r_in.direction(), rec.normal);
    float ni_over_nt;
    attenuation = vec3(1.0, 1.0, 1.0);
    vec3 refracted;

    float reflect_prob;
    float cosine;

    if (dot(r_in.direction(), rec.normal) > 0) {
      outward_normal = -rec.normal;
      ni_over_nt = ref_idx;
      cosine = ref_idx * dot(r_in.direction(), rec.normal)
        / r_in.direction().length();
    }
    else {
      outward_normal = rec.normal;
      ni_over_nt = 1.0 / ref_idx;
      cosine = -dot(r_in.direction(), rec.normal)
        / r_in.direction().length();
    }

    if (refract(r_in.direction(), outward_normal, ni_over_nt, refracted)) {
      reflect_prob = schlick(cosine, ref_idx);
    }
    else {
      reflect_prob = 1.0;
    }

    if (random_double() < reflect_prob) {
      scattered = ray(rec.p, reflected);
    }
    else {
      scattered = ray(rec.p, refracted);
    }

    return true;
  }

  float ref_idx;
};


#endif
